#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
'''
Created on 20/08/2013

@author: Jesper
'''

import imaplib
import socket
import re
import time

import MyIMAP4_SSL
import translation; translation.installTranslation()
import messagefunctions 
from messagefunctions import _makeMessageIdAsciiOnly, \
    _getMessageObjectFromMessageAsString, _getMessageIdFromMessageObject, \
    imap_unescape


_submoduleListForVersioning = [MyIMAP4_SSL, translation, messagefunctions]
import versioning; _revisionInfo = versioning.getRevisionInfoFromTagsAndSubModules('$Revision: 133 $', '$Date: 2013-10-16 00:19:08 +0000 (Wed, 16 Oct 2013) $', _submoduleListForVersioning)


NETWORK_ERROR_FIRST_RECONNECT_ATTEMPT_SECONDS_TO_SLEEP = 20 # After first network error, sleep for this many seconds.
MAXIMUM_NUMBER_OF_RECONNECTS = 5 # Maximum number of reconnect attempts after network failure.


MESSAGE_LANGUAGE_NOT_SUPPORTED = _(\
'''Your Gmail account doesn't export some IMAP needed to make Gmail Backup work.

Possible causes are:

- You are using Gmail Labs.
  Please go to the Settings/Label page and enable IMAP access into
  All Mails and Trash folders.

- You are using an unsupported language of Gmail. If you are using Gmail Backup
  under Windows, please run the following command:

    gmail-backup.exe list <your_address@gmail.com> <your_password>

  and send the output of this command to our user support group:
  gmail-backup-com-users@googlegroups.com

  Thank you''')

class GmailConnection(object):
    imapFolderNameAllMail = None
    imapFolderNameTrash = None

    # JH 2013-08-04: 
    #     See https://developers.google.com/gmail/imap_extensions#special-use_extension_of_the_list_command
    #     for a much better way of handling this, actually handling all languages. 
    #     Knowledge of the language is not needed when using the Gmail Imap 
    #     extensions shown there.
    #
    #     I hope the Python Imap library can handle it.
    #
    #     See also http://tools.ietf.org/html/rfc6154 for the special use
    #     extensions to Imap that Gmail provides (cf. 
    #     https://developers.google.com/gmail/imap_extensions#xlist_is_deprecated)
    #
    # JH 2013-08-04: 
    #     See also https://developers.google.com/gmail/imap_extensions#access_to_the_gmail_unique_message_id_x-gm-msgid
    #     for a way to get safely unique IDs of messages (X-GM-MSGID).
    #
    # JH 2013-08-04: 
    #    See also https://developers.google.com/gmail/imap_extensions#access_to_gmail_labels_x-gm-labels
    #     which provides access to all the labels for a specific message.

    MAILBOX_NAMES_IMAP_UTF7_ENCODED = {
        'en_us': ('[Gmail]/All Mail', '[Gmail]/Trash'),
        'en_uk': ('[Gmail]/All Mail', '[Gmail]/Bin'),
        'cs': ('[Gmail]/V&AWE-echny zpr&AOE-vy', '[Gmail]/Ko&AWE-'),
        'es': ('[Gmail]/Todos', '[Gmail]/Papelera'),
        'de': ('[Gmail]/Alle Nachrichten', '[Gmail]/Papierkorb'),
        'fr': ('[Gmail]/Tous les messages', '[Gmail]/Corbeille'),
        'ar': ('[Gmail]/&BkMGRA- &BicGRAYoBjEGSgYv-', '[Gmail]/&BkUGRwZABkUGRAYnBio-'),
        'it': ('[Gmail]/Tutti i messaggi', '[Gmail]/Cestino'),
        'pl': ('[Gmail]/Wszystkie', '[Gmail]/Kosz'),
        'sk': ('[Gmail]/V&AWE-etky spr&AOE-vy', '[Gmail]/K&APQBYQ-'),
        'bahasa_indonesia': ('[Gmail]/Semua Email', '[Gmail]/Tong Sampah'),
        'bahasa_melayu': ('[Gmail]/Semua Mel', '[Gmail]/Sampah'),
        'catala': ('[Gmail]/Tots els missatges', '[Gmail]/Paperera'),
        'dansk': ('[Gmail]/Alle e-mails', '[Gmail]/Papirkurv'),
        'eesti_keel': ('[Gmail]/K&APU-ik kirjad', '[Gmail]/Pr&APw-gikast'),
        'filipino': ('[Gmail]/Lahat ng Mail', '[Gmail]/Basurahan'),
        'hrvatski': ('[Gmail]/Sva po&AWE-ta', '[Gmail]/Otpad'),
        'islenska': ('[Gmail]/Allur p&APM-stur', '[Gmail]/Rusl'),
        'latviesu': ('[Gmail]/Visas v&ARM-stules', '[Gmail]/Miskaste'),
        'lietuviu': ('[Gmail]/Visi lai&AWE-kai', '[Gmail]/&AWA-iuk&AWE-liad&ARcBfgEX-'),
        'magyar': ('[Gmail]/&ANY-sszes lev&AOk-l', '[Gmail]/Kuka'),
        'norsk': ('[Gmail]/All e-post', '[Gmail]/Papirkurv'),
        'nederlands': ('[Gmail]/Alle berichten', '[Gmail]/Prullenbak'),
        'portugues_brazil': ('[Gmail]/Todos os e-mails', '[Gmail]/Lixeira'),
        'portugues_portugal': ('[Gmail]/Todo o correio', '[Gmail]/Lixo'),
        'romana': ('[Gmail]/Toate mesajele', '[Gmail]/Co&AV8- de gunoi'),
        'slovenscina': ('[Gmail]/Vsa po&AWE-ta', '[Gmail]/Smetnjak'),
        'suomi': ('[Gmail]/Kaikki viestit', '[Gmail]/Roskakori'),
        'svenska': ('[Gmail]/Alla mail', '[Gmail]/Papperskorgen'),
        'tieng_viet': ('[Gmail]/T&AOIDAQ-t ca&Awk- Th&AbA-', '[Gmail]/Thu&AwA-ng Ra&AwE-c'),
        'turkce': ('[Gmail]/T&APw-m Postalar', '[Gmail]/&AMcA9g-p Kutusu'),
        'ellnvika': ('[Gmail]/&A4wDuwOx- &A8QDsQ- &A7wDtwO9A80DvAOxA8QDsQ-', '[Gmail]/&A5oDrAO0A78Dwg- &A7EDwAO,A8EDwQO5A7wDvAOsA8QDyQO9-'),
        'azbuka1': ('[Gmail]/&BBIEQQRP- &BD8EPgRHBEIEMA-', '[Gmail]/&BBoEPgRABDcEOAQ9BDA-'),
        'azbuka2': ('[Gmail]/&BCEEMgQw- &BD8EPgRIBEIEMA-', '[Gmail]/&BB4EQgQ,BDAENA-'),
        'azbuka3': ('[Gmail]/&BCMEQQRP- &BD8EPgRIBEIEMA-', '[Gmail]/&BBoEPgRIBDgEOg-'),
        'azbuka4': ('[Gmail]/&BCYETwQ7BDAEQgQw- &BD8EPgRJBDA-', '[Gmail]/&BBoEPgRIBEcENQ-'),
        'hebrew': ('[Gmail]/&BdsF3A- &BdQF0wXVBdAF6A-', '[Gmail]/&BdAF6QXkBdQ-'),
        'arabic': ('[Gmail]/&BkMGRA- &BicGRAYoBjEGSgYv-', '[Gmail]/&BkUGRwZABkUGRAYnBio-'),
        'caj1': ('[Gmail]/&CTgJLQlA- &CS4JRwky-', '[Gmail]/&CR8JTQkwCUgJNg- &CRUJMAlHCQI-'),
        'caj2': ('[Gmail]/&DggOFA4rDiEOMg4iDhcOMQ5JDgcOKw4hDhQ-', '[Gmail]/&DhYOMQ4HDgIOIg4w-'),
        'caj3': ('[Gmail]/&UWiQ6JD1TvY-', '[Gmail]/&V4NXPmh2-'),
        'caj4': ('[Gmail]/&YkBnCZCuTvY-', '[Gmail]/&XfJSIJZkkK5O9g-'),
        'caj5': ('[Gmail]/&MFkweTBmMG4w4TD8MOs-', '[Gmail]/&MLQw33ux-'),
        'caj6': ('[Gmail]/&yATMtLz0rQDVaA-', '[Gmail]/&1zTJwNG1-'),
        
        # The same with Google Mail instead of Gmail
        'en_us_GM': ('[Google Mail]/All Mail', '[Google Mail]/Trash'),
        'en_uk_GM': ('[Google Mail]/All Mail', '[Google Mail]/Bin'),
        'cs_GM': ('[Google Mail]/V&AWE-echny zpr&AOE-vy', '[Google Mail]/Ko&AWE-'),
        'es_GM': ('[Google Mail]/Todos', '[Google Mail]/Papelera'),
        'de_GM': ('[Google Mail]/Alle Nachrichten', '[Google Mail]/Papierkorb'),
        'fr_GM': ('[Google Mail]/Tous les messages', '[Google Mail]/Corbeille'),
        'ar_GM': ('[Google Mail]/&BkMGRA- &BicGRAYoBjEGSgYv-', '[Google Mail]/&BkUGRwZABkUGRAYnBio-'),
        'it_GM': ('[Google Mail]/Tutti i messaggi', '[Google Mail]/Cestino'),
        'pl_GM': ('[Google Mail]/Wszystkie', '[Google Mail]/Kosz'),
        'sk_GM': ('[Google Mail]/V&AWE-etky spr&AOE-vy', '[Google Mail]/K&APQBYQ-'),
        'bahasa_indonesia_GM': ('[Google Mail]/Semua Email', '[Google Mail]/Tong Sampah'),
        'bahasa_melayu_GM': ('[Google Mail]/Semua Mel', '[Google Mail]/Sampah'),
        'catala_GM': ('[Google Mail]/Tots els missatges', '[Google Mail]/Paperera'),
        'dansk_GM': ('[Google Mail]/Alle e-mails', '[Google Mail]/Papirkurv'),
        'eesti_keel_GM': ('[Google Mail]/K&APU-ik kirjad', '[Google Mail]/Pr&APw-gikast'),
        'filipino_GM': ('[Google Mail]/Lahat ng Mail', '[Google Mail]/Basurahan'),
        'hrvatski_GM': ('[Google Mail]/Sva po&AWE-ta', '[Google Mail]/Otpad'),
        'islenska_GM': ('[Google Mail]/Allur p&APM-stur', '[Google Mail]/Rusl'),
        'latviesu_GM': ('[Google Mail]/Visas v&ARM-stules', '[Google Mail]/Miskaste'),
        'lietuviu_GM': ('[Google Mail]/Visi lai&AWE-kai', '[Google Mail]/&AWA-iuk&AWE-liad&ARcBfgEX-'),
        'magyar_GM': ('[Google Mail]/&ANY-sszes lev&AOk-l', '[Google Mail]/Kuka'),
        'norsk_GM': ('[Google Mail]/All e-post', '[Google Mail]/Papirkurv'),
        'nederlands_GM': ('[Google Mail]/Alle berichten', '[Google Mail]/Prullenbak'),
        'portugues_brazil_GM': ('[Google Mail]/Todos os e-mails', '[Google Mail]/Lixeira'),
        'portugues_portugal_GM': ('[Google Mail]/Todo o correio', '[Google Mail]/Lixo'),
        'romana_GM': ('[Google Mail]/Toate mesajele', '[Google Mail]/Co&AV8- de gunoi'),
        'slovenscina_GM': ('[Google Mail]/Vsa po&AWE-ta', '[Google Mail]/Smetnjak'),
        'suomi_GM': ('[Google Mail]/Kaikki viestit', '[Google Mail]/Roskakori'),
        'svenska_GM': ('[Google Mail]/Alla mail', '[Google Mail]/Papperskorgen'),
        'tieng_viet_GM': ('[Google Mail]/T&AOIDAQ-t ca&Awk- Th&AbA-', '[Google Mail]/Thu&AwA-ng Ra&AwE-c'),
        'turkce_GM': ('[Google Mail]/T&APw-m Postalar', '[Google Mail]/&AMcA9g-p Kutusu'),
        'ellnvika_GM': ('[Google Mail]/&A4wDuwOx- &A8QDsQ- &A7wDtwO9A80DvAOxA8QDsQ-', '[Google Mail]/&A5oDrAO0A78Dwg- &A7EDwAO,A8EDwQO5A7wDvAOsA8QDyQO9-'),
        'azbuka1_GM': ('[Google Mail]/&BBIEQQRP- &BD8EPgRHBEIEMA-', '[Google Mail]/&BBoEPgRABDcEOAQ9BDA-'),
        'azbuka2_GM': ('[Google Mail]/&BCEEMgQw- &BD8EPgRIBEIEMA-', '[Google Mail]/&BB4EQgQ,BDAENA-'),
        'azbuka3_GM': ('[Google Mail]/&BCMEQQRP- &BD8EPgRIBEIEMA-', '[Google Mail]/&BBoEPgRIBDgEOg-'),
        'azbuka4_GM': ('[Google Mail]/&BCYETwQ7BDAEQgQw- &BD8EPgRJBDA-', '[Google Mail]/&BBoEPgRIBEcENQ-'),
        'hebrew_GM': ('[Google Mail]/&BdsF3A- &BdQF0wXVBdAF6A-', '[Google Mail]/&BdAF6QXkBdQ-'),
        'arabic_GM': ('[Google Mail]/&BkMGRA- &BicGRAYoBjEGSgYv-', '[Google Mail]/&BkUGRwZABkUGRAYnBio-'),
        'caj1_GM': ('[Google Mail]/&CTgJLQlA- &CS4JRwky-', '[Google Mail]/&CR8JTQkwCUgJNg- &CRUJMAlHCQI-'),
        'caj2_GM': ('[Google Mail]/&DggOFA4rDiEOMg4iDhcOMQ5JDgcOKw4hDhQ-', '[Google Mail]/&DhYOMQ4HDgIOIg4w-'),
        'caj3_GM': ('[Google Mail]/&UWiQ6JD1TvY-', '[Google Mail]/&V4NXPmh2-'),
        'caj4_GM': ('[Google Mail]/&YkBnCZCuTvY-', '[Google Mail]/&XfJSIJZkkK5O9g-'),
        'caj5_GM': ('[Google Mail]/&MFkweTBmMG4w4TD8MOs-', '[Google Mail]/&MLQw33ux-'),
        'caj6_GM': ('[Google Mail]/&yATMtLz0rQDVaA-', '[Google Mail]/&1zTJwNG1-'),
    }

    def __init__(self, username, password, notifier, lang=None):
        self.username = username
        self.password = password
        self.notifier = notifier
        self.languageCode = None
        if lang is not None:
            self.setLanguage(lang)
        self._lastMailbox = None
        self._lastSearch = None
        self._lastFetchedMessageNumber = None
        self._lastFetchedMessageAsString = None
        self._loggedIn = False
        self.imapConnection = None

    def isRecoverableError(self, exception):
        '''Returns True if the given exception is a recoverable network error, otherwise False.'''
        if isinstance(exception, (socket.error, imaplib.IMAP4_SSL.abort, socket.timeout)):
            isRecoverable = True
        elif isinstance(exception, imaplib.IMAP4_SSL.error):
            # This is recoverable if we are already logged in but
            # get "Invalid credentials" anyway.
            isRecoverable = self._loggedIn and 'Invalid credentials' in str(exception)
        else:
            isRecoverable = False
    
        return isRecoverable
    
    def _findLanguageWhereAllMailAndTrashMatches(self, presentMailboxSet):
        '''Try to find a language where both "All Mail" and "Trash" folders match.
        Return the found languageCode, or None if no matching language was found.'''
        for languageCode, (allMailFolderName, trashFolderName) in self.MAILBOX_NAMES_IMAP_UTF7_ENCODED.iteritems():
            if allMailFolderName in presentMailboxSet and trashFolderName in presentMailboxSet:
                foundLanguageCode = languageCode
                break
        else:  # Yes, this else belongs to the for loop.
            # Nothing found.
            foundLanguageCode = None
        
        return foundLanguageCode 


    def _findLanguageWhereAllMailMatches(self, presentMailboxSet):
        '''Try to find a language where just the "All Mail" folder match.
        Returns the found languageCode or None if no matching language found.'''
        for languageCode, (allMailFolderName, trashFolderName) in self.MAILBOX_NAMES_IMAP_UTF7_ENCODED.iteritems(): # @UnusedVariable trashFolderName
            if allMailFolderName in presentMailboxSet:
                self.notifier.notifyLog("Guessing language with internal code '%s', in case of problems contact us at honza.svec@gmail.com" % languageCode)
                foundLanguageCode = languageCode
                break
        else:  # Yes, this else belongs to the for loop.
            # Nothing found.
            foundLanguageCode = None

        return foundLanguageCode
    
    
    def guessLanguage(self):
        '''Try to guess the language of the Gmail account.'''

        # Create set of present mailboxes, presentMailboxSetImapUtf7Encoded, for the connection.
        presentMailboxSetImapUtf7Encoded = set()  # Start with an empty set.
        status, mailboxList = self.imapConnection.list()  # @UnusedVariable status
        for mailbox in mailboxList:
            match = re.match(r'^\(.*\)\s".*"\s"(\[.*\].*)"\s*$', mailbox)
            if match:
                mailboxNameImapUtf7Encoded = match.group(1)  # This has the form "[Gmail]/All Mail", where "Gmail" and "All Mail" can vary.
                presentMailboxSetImapUtf7Encoded.add(mailboxNameImapUtf7Encoded)

        foundLanguageCode = self._findLanguageWhereAllMailAndTrashMatches(presentMailboxSetImapUtf7Encoded)

        if not foundLanguageCode:
            # Language not found. 
            # Try to find a language that at least matches the "All mail" folder.
            foundLanguageCode = self._findLanguageWhereAllMailMatches(presentMailboxSetImapUtf7Encoded)

        if not foundLanguageCode:
            # Still no language found.
            self.notifier.notifyError(MESSAGE_LANGUAGE_NOT_SUPPORTED)
            raise ValueError("Cannot access IMAP folders")
        
        return foundLanguageCode  # This will always be a found languageCode.

    def setLanguage(self, languageCode):
        self.languageCode = languageCode
        self.imapFolderNameAllMail = self.MAILBOX_NAMES_IMAP_UTF7_ENCODED[languageCode][0]
        self.imapFolderNameTrash = self.MAILBOX_NAMES_IMAP_UTF7_ENCODED[languageCode][1]
    
    
    def connect(self, guessLanguage=True):
        '''
        Connect or reconnect to IMAP. 
        
        Note that in case of network problems, this method might 
        also be called in the middle of a backup or restore if there 
        are network problems, to try to reconnect. A reconnect will 
        change the value of self.imapConnection. The value of 
        self.imapConnection can therefore change in the middle of 
        an operation such as a backup or a restore.
        '''
        GMAIL_IMAP_ADDRESS = 'imap.gmail.com'
        GMAIL_IMAP_PORT = 993
        self.imapConnection = MyIMAP4_SSL.MyIMAP4_SSL(GMAIL_IMAP_ADDRESS, GMAIL_IMAP_PORT)
        self.imapConnection.setNotifier(self.notifier)
        self.imapConnection.login(self.username, self.password)
        self._loggedIn = True
        if self.languageCode is None and guessLanguage:
            languageCode = self.guessLanguage()
            self.setLanguage(languageCode)

    def close(self):
        '''
        Close and delete the IMAP connection.
        '''
        self.imapConnection.shutdown()
        del self.imapConnection

    def selectMailbox(self, mailbox):
        self._lastMailbox = mailbox
        self._callImapConnectionMethodWithReconnect("select", mailbox)

    def selectMailboxAllMail(self):
        self.selectMailbox(self.imapFolderNameAllMail)


    def interruptibleSleep(self, secondsToSleep):
        '''
        Sleep for the given number of seconds in a way that is 
        supposed to be interruptible by an externally induced exception 
        to the thread, such as a KeyboardInterrupt or SystemExit.
        
        JH 2013-10-07: DOES NOT SEEM to be interruptible yet... 
        Is there some way to get a signal that can be polled? Or 
        is there a special call or action that can be made that will for sure 
        make an asynchronuous exception go through? Some file reading 
        for instance?
        
        Or maybe a flag in this object can be set by the main thread 
        and then be polled in this routine?
        
        See e.g. http://stackoverflow.com/questions/4606942/why-cant-i-handle-a-keyboardinterrupt-in-python
        about the problems with asynchronous exception raising in Python.
        
        
        :param secondsToSleep:
            Approximate number of seconds to sleep in total.
        :type secondsToSleep:
            Integer or float.
        '''
        MICRONAP_SECONDS = 1  # Time to sleep in each micronap. Can be a floating point number.
        endTime = time.time() + secondsToSleep
        try:
            # time.sleep() seems to be definitely non-interruptible
            # by exceptions. So we try to do something else between
            # the sleeps to give an eventual exception a chance.
            while time.time() < endTime:
                # Take a small nap.
                time.sleep(MICRONAP_SECONDS)
                #with file("C:\\bootmgr", "r") as f:
                #    f.close()
        except:
            # The sleep was aborted by an exception.
            self.notifier.notifyLog(_("Sleep aborted."))
            raise

    
    def attemptToReconnect(self):
        '''
        Attempt to reconnect after network failure. 
        
        Returns:
            True if reconnect was successful.
            False otherwise.
        '''
        
        attemptNumber = 0
        secondsToSleep = NETWORK_ERROR_FIRST_RECONNECT_ATTEMPT_SECONDS_TO_SLEEP
        reconnectSucceeded = False
        while attemptNumber < MAXIMUM_NUMBER_OF_RECONNECTS and not reconnectSucceeded:
            attemptNumber += 1
            self.notifier.notifyLog(_("Trying to reconnect (%d)") % attemptNumber)
            try:
                self.connect()
                if self._lastMailbox:
                    self.selectMailbox(self._lastMailbox)
                if self._lastSearch:
                    self.search(self._lastSearch)
                self.notifier.notifyLog(_("Reconnected!"))
                reconnectSucceeded = True
            except Exception as exception:
                if self.isRecoverableError(exception):
                    self.notifier.notifyLog(_("Not connected, sleeping for %d seconds...") % secondsToSleep)

                    # Take a nap.
                    # Note that after several failures, the following nap can grow to 
                    # 2**(MAXIMUM_NUMBER_OF_RECONNECTS-1) * NETWORK_ERROR_FIRST_RECONNECT_ATTEMPT_SECONDS_TO_SLEEP seconds 
                    # = 2**4 * 20 seconds = 16*20 seconds = 320 seconds = 5.3 minutes.
                    # This is a long wait, so it has to be abortable by the user.
                    # Therefore this special call to a method that is supposed to be 
                    # interruptable – but maybe it actually isn't.
                    self.interruptibleSleep(secondsToSleep)

                    # Sleep for twice as long after next failed attempt, if any.
                    secondsToSleep *= 2  

                else:
                    # The exception is non-recoverable.
                    # Re-raise the exception.
                    raise

        if not reconnectSucceeded:
            self.notifier.notifyLog(_("Unable to reconnect."))

        return reconnectSucceeded

    def fetchMessageId(self, messageNumber):
        typ, data = self._callImapConnectionMethodWithReconnect("fetch", messageNumber, '(BODY.PEEK[HEADER.FIELDS (Message-ID)])')  # @UnusedVariable typ
        if data is None or data[0] is None:
            match = None
        else:
            match = re.match(r'^.*:\s*<(.*)>$', data[0][1].strip())
            
        if match:
            # The message has a Message-ID field stored in it.
            messageId = _makeMessageIdAsciiOnly(match.group(1))
        else:
            # The message does not have a Message-ID field stored in it.
            # We compute our synthetic Message-ID from the whole message.
            messageAsString = self.fetchMessageAsString(messageNumber)
            messageObject = _getMessageObjectFromMessageAsString(messageAsString)
            messageId = _getMessageIdFromMessageObject(messageObject)

        return messageId

    def fetchMessageAsString(self, messageNumber):
        if self._lastFetchedMessageNumber == messageNumber:
            # We have it cached.
            messageAsString = self._lastFetchedMessageAsString
        else:
            # We don't have it cached.
            typ, data = self._callImapConnectionMethodWithReconnect("fetch", messageNumber, '(BODY.PEEK[])')  # @UnusedVariable typ
            messageAsString = data[0][1]
            self._lastFetchedMessageNumber = messageNumber
            self._lastFetchedMessageAsString = messageAsString

        return messageAsString
    
    def search(self, where):
        self._lastSearch = where
        typ, numbers = self._callImapConnectionMethodWithReconnect("search", None, *where)  # @UnusedVariable typ
        numbers = numbers[0].split()
        return numbers


    def _getMailboxNamesUsingMethod(self, methodName):
        '''methodName should be either "lsub" or "list".'''
        assert methodName in ["lsub", "list"]
        status, listOfMailboxNamesImapEscaped = self._callImapConnectionMethodWithReconnect(methodName) # @UnusedVariable status
        listOfMailboxNamesUnescaped = [imap_unescape(mailboxNameImapEscaped) for mailboxNameImapEscaped in listOfMailboxNamesImapEscaped]
        return listOfMailboxNamesUnescaped

    def lsub(self):
        '''Return list of 'subscribed' mailbox names, unescaped, as a list of strings.'''
        listOfSubscribedMailboxNamesUnescaped = self._getMailboxNamesUsingMethod("lsub")
        return listOfSubscribedMailboxNamesUnescaped

    def list(self):
        '''Return list of all mailbox names, unescaped, as a list of strings.'''
        listOfMailboxNamesUnescaped = self._getMailboxNamesUsingMethod("list")
        return listOfMailboxNamesUnescaped

    def create(self, label):
        self._callImapConnectionMethodWithReconnect("create", label)

    def copy(self, message_set, label):
        self._callImapConnectionMethodWithReconnect("copy", message_set, label)

    def append(self, mailbox, flags, msg_date, msg):
        self._callImapConnectionMethodWithReconnect("append", mailbox, flags, msg_date, msg)

    def store(self, nums, state, flags):
        self._callImapConnectionMethodWithReconnect("store", nums, state, flags)

    def expunge(self):
        self._callImapConnectionMethodWithReconnect("expunge")

    def delete(self, mailbox):
        self._callImapConnectionMethodWithReconnect("delete", mailbox)

    def _callImapConnectionMethodWithReconnect(self, methodName, *args, **kwargs):
        '''Call the method with the given name methodName on self.imapConnection, with
        the given arguments args and keyword arguments kwargs.
        
        JH: Note that we use the *name* as reference and *not* a direct method reference from
        self.imapConnection (such as self.imapConnection.list). This is because we want to 
        be able to recover from a network error, and this requires (?) that we create
        a new connection object for each retry. This new connection object does not (?) have
        the same method reference as the old connection object. Therefore we use
        the method *name* and not the method pointer as a stable and safe reference.'''
        
        successful = False
        while not successful:

            methodReference = getattr(self.imapConnection, methodName)
            
            try:
                returnValue = methodReference(*args, **kwargs)
            except Exception as exception:
                if self.isRecoverableError(exception):
                    self.notifier.notifyLog(_("Network error occurred, disconnected"))
                    reconnectWasSuccessful = self.attemptToReconnect()  # Note that this changes self.imapConnection.
                    if not reconnectWasSuccessful:
                        raise
                else:
                    raise
            else:
                successful = True

        return returnValue

